/******************************************************************************
 * Copyright (c) 2017 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 ******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.json.adapters;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Tuple;

import com.google.gson.reflect.TypeToken;

/**
 * Utilities for handling types in the JSON parser / serializer.
 */
public final class TypeUtils {

	private TypeUtils() {}

	/**
	 * Determine the actual type arguments of the given type token with regard to the given target type.
	 */
	public static Type[] getElementTypes(TypeToken<?> typeToken, Class<?> targetType) {
		return getElementTypes(typeToken.getType(), typeToken.getRawType(), targetType);
	}

	private static Type[] getElementTypes(Type type, Class<?> rawType, Class<?> targetType) {
		if (targetType.equals(rawType) && type instanceof ParameterizedType) {
			Type mappedType;
			if (type instanceof ParameterizedTypeImpl)
				mappedType = type;
			else
				// Transform wildcards in the actual type arguments
				mappedType = getMappedType(type, Collections.emptyMap());
			return ((ParameterizedType) mappedType).getActualTypeArguments();
		}
		// Map the parameters of the raw type to the actual type arguments
		Map<String, Type> varMapping = createVariableMapping(type, rawType);
		if (targetType.isInterface()) {
			// Look for superinterfaces that extend the target interface
			Class<?>[] interfaces = rawType.getInterfaces();
			for (int i = 0; i < interfaces.length; i++) {
				if (Collection.class.isAssignableFrom(interfaces[i])) {
					Type genericInterface = rawType.getGenericInterfaces()[i];
					Type mappedInterface = getMappedType(genericInterface, varMapping);
					return getElementTypes(mappedInterface, interfaces[i], targetType);
				}
			}
		}
		if (!rawType.isInterface()) {
			// Visit the superclass if it extends the target class / implements the target interface
			Class<?> rawSupertype = rawType.getSuperclass();
			if (targetType.isAssignableFrom(rawSupertype)) {
				Type genericSuperclass = rawType.getGenericSuperclass();
				Type mappedSuperclass = getMappedType(genericSuperclass, varMapping);
				return getElementTypes(mappedSuperclass, rawSupertype, targetType);
			}
		}
		// No luck, return an array of Object types
		final var result = new Type[targetType.getTypeParameters().length];
		Arrays.fill(result, Object.class);
		return result;
	}

	private static <T> Map<String, Type> createVariableMapping(Type type, Class<T> rawType) {
		if (type instanceof ParameterizedType) {
			TypeVariable<Class<T>>[] vars = rawType.getTypeParameters();
			Type[] args = ((ParameterizedType) type).getActualTypeArguments();
			final var newVarMapping = new HashMap<String, Type>(capacity(vars.length));
			for (int i = 0; i < vars.length; i++) {
				Type actualType = Object.class;
				if (i < args.length) {
					actualType = args[i];
					if (actualType instanceof WildcardType)
						actualType = ((WildcardType) actualType).getUpperBounds()[0];
				}
				newVarMapping.put(vars[i].getName(), actualType);
			}
			return newVarMapping;
		}
		return Collections.emptyMap();
	}

	private static int capacity(int expectedSize) {
		if (expectedSize < 3)
			return expectedSize + 1;
		return expectedSize + expectedSize / 3;
	}

	private static Type getMappedType(Type type, Map<String, Type> varMapping) {
		if (type instanceof TypeVariable) {
			String name = ((TypeVariable<?>) type).getName();
			if (varMapping.containsKey(name))
				return varMapping.get(name);
		}
		if (type instanceof WildcardType) {
			return getMappedType(((WildcardType) type).getUpperBounds()[0], varMapping);
		}
		if (type instanceof ParameterizedType) {
			ParameterizedType pt = (ParameterizedType) type;
			Type[] origArgs = pt.getActualTypeArguments();
			Type[] mappedArgs = new Type[origArgs.length];
			for (int i = 0; i < origArgs.length; i++) {
				mappedArgs[i] = getMappedType(origArgs[i], varMapping);
			}
			return new ParameterizedTypeImpl(pt, mappedArgs);
		}
		return type;
	}

	private static class ParameterizedTypeImpl implements ParameterizedType {

		private final Type ownerType;
		private final Type rawType;
		private final Type[] actualTypeArguments;

		ParameterizedTypeImpl(ParameterizedType original, Type[] typeArguments) {
			this(original.getOwnerType(), original.getRawType(), typeArguments);
		}

		ParameterizedTypeImpl(Type ownerType, Type rawType, Type[] typeArguments) {
			this.ownerType = ownerType;
			this.rawType = rawType;
			this.actualTypeArguments = typeArguments;
		}

		@Override
		public Type getOwnerType() {
			return ownerType;
		}

		@Override
		public Type getRawType() {
			return rawType;
		}

		@Override
		public Type[] getActualTypeArguments() {
			return actualTypeArguments;
		}

		@Override
		public String toString() {
			final var result = new StringBuilder();
			if (ownerType != null) {
				result.append(toString(ownerType));
				result.append('$');
			}
			result.append(toString(rawType));
			result.append('<');
			for (int i = 0; i < actualTypeArguments.length; i++) {
				if (i > 0)
					result.append(", ");
				result.append(toString(actualTypeArguments[i]));
			}
			result.append('>');
			return result.toString();
		}

		private String toString(Type type) {
			if (type instanceof Class<?>)
				return ((Class<?>) type).getName();
			return String.valueOf(type);
		}

	}

	/**
	 * Return all possible types that can be expected when an element of the given type is parsed.
	 * If the type satisfies {@link #isEither(Type)}, a list of the corresponding type arguments is returned,
	 * otherwise a list containg the type itself is returned. Type parameters are <em>not</em> resolved
	 * by this method (use {@link #getElementTypes(TypeToken, Class)} to get resolved parameters).
	 */
	public static Collection<Type> getExpectedTypes(Type type) {
		final var result = new ArrayList<Type>();
		collectExpectedTypes(type, result);
		return result;
	}

	private static void collectExpectedTypes(Type type, Collection<Type> types) {
		if (isEither(type)) {
			if (type instanceof ParameterizedType) {
				for (Type typeArgument : ((ParameterizedType) type).getActualTypeArguments()) {
					collectExpectedTypes(typeArgument, types);
				}
			}
			if (type instanceof Class) {
				for (Type typeParameter : ((Class<?>) type).getTypeParameters()) {
					collectExpectedTypes(typeParameter, types);
				}
			}
		} else {
			types.add(type);
		}
	}

	/**
	 * Test whether the given type is Either.
	 */
	public static boolean isEither(Type type) {
		if (type instanceof ParameterizedType) {
			return isEither(((ParameterizedType) type).getRawType());
		}
		if (type instanceof Class) {
			return Either.class.isAssignableFrom((Class<?>) type);
		}
		return false;
	}

	/**
	 * Test whether the given type is a two-tuple (pair).
	 */
	public static boolean isTwoTuple(Type type) {
		if (type instanceof ParameterizedType) {
			return isTwoTuple(((ParameterizedType) type).getRawType());
		}
		if (type instanceof Class) {
			return Tuple.Two.class.isAssignableFrom((Class<?>) type);
		}
		return false;
	}

}
